package com.shm.leetcode;

import java.util.*;

/**
 * 332. 重新安排行程
 * 给定一个机票的字符串二维数组 [from, to]，子数组中的两个成员分别表示飞机出发和降落的机场地点，对该行程进行重新规划排序。所有这些机票都属于一个从 JFK（肯尼迪国际机场）出发的先生，所以该行程必须从 JFK 开始。
 *
 * 说明:
 *
 * 如果存在多种有效的行程，你可以按字符自然排序返回最小的行程组合。例如，行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小，排序更靠前
 * 所有的机场都用三个大写字母表示（机场代码）。
 * 假定所有机票至少存在一种合理的行程。
 * 示例 1:
 *
 * 输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
 * 输出: ["JFK", "MUC", "LHR", "SFO", "SJC"]
 * 示例 2:
 *
 * 输入: [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
 * 输出: ["JFK","ATL","JFK","SFO","ATL","SFO"]
 * 解释: 另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。
 * @author SHM
 */
public class FindItinerary {
    Map<String, PriorityQueue<String>> map = new HashMap<>();
    List<String> res = new LinkedList<>();

    /**
     * 前言
     * 本题和 753. 破解保险箱 类似，是力扣平台上为数不多的求解欧拉回路 / 欧拉通路的题目。读者可以配合着进行练习。
     *
     * 我们化简本题题意：给定一个 nn 个点 mm 条边的图，要求从指定的顶点出发，经过所有的边恰好一次（可以理解为给定起点的「一笔画」问题），使得路径的字典序最小。
     *
     * 这种「一笔画」问题与欧拉图或者半欧拉图有着紧密的联系，下面给出定义：
     *
     * 通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉通路。
     *
     * 通过图中所有边恰好一次且行遍所有顶点的回路称为欧拉回路。
     *
     * 具有欧拉回路的无向图称为欧拉图。
     *
     * 具有欧拉通路但不具有欧拉回路的无向图称为半欧拉图。
     *
     * 因为本题保证至少存在一种合理的路径，也就告诉了我们，这张图是一个欧拉图或者半欧拉图。我们只需要输出这条欧拉通路的路径即可。
     *
     * 如果没有保证至少存在一种合理的路径，我们需要判别这张图是否是欧拉图或者半欧拉图，具体地：
     *
     * 对于无向图 GG，GG 是欧拉图当且仅当 GG 是连通的且没有奇度顶点。
     *
     * 对于无向图 GG，GG 是半欧拉图当且仅当 GG 是连通的且 GG 中恰有 22 个奇度顶点。
     *
     * 对于有向图 GG，GG 是欧拉图当且仅当 GG 的所有顶点属于同一个强连通分量且每个顶点的入度和出度相同。
     *
     * 对于有向图 GG，GG 是半欧拉图当且仅当 GG 的所有顶点属于同一个强连通分量且
     *
     * 恰有一个顶点的出度与入度差为 11；
     *
     * 恰有一个顶点的入度与出度差为 11；
     *
     * 所有其他顶点的入度和出度相同。
     *
     * 思路及算法
     *
     * \text{Hierholzer}Hierholzer 算法用于在连通图中寻找欧拉路径，其流程如下：
     *
     * 从起点出发，进行深度优先搜索。
     *
     * 每次沿着某条边从某个顶点移动到另外一个顶点的时候，都需要删除这条边。
     *
     * 如果没有可移动的路径，则将所在节点加入到栈中，并返回。
     *
     * 当我们顺序地考虑该问题时，我们也许很难解决该问题，因为我们无法判断当前节点的哪一个分支是「死胡同」分支。
     *
     * 不妨倒过来思考。我们注意到只有那个入度与出度差为 11 的节点会导致死胡同。而该节点必然是最后一个遍历到的节点。我们可以改变入栈的规则，当我们遍历完一个节点所连的所有节点后，我们才将该节点入栈（即逆序入栈）。
     *
     * 对于当前节点而言，从它的每一个非「死胡同」分支出发进行深度优先搜索，都将会搜回到当前节点。而从它的「死胡同」分支出发进行深度优先搜索将不会搜回到当前节点。也就是说当前节点的死胡同分支将会优先于其他非「死胡同」分支入栈。
     *
     * 这样就能保证我们可以「一笔画」地走完所有边，最终的栈中逆序地保存了「一笔画」的结果。我们只要将栈中的内容反转，即可得到答案。
     *
     * 作者：LeetCode-Solution
     * 链接：https://leetcode-cn.com/problems/reconstruct-itinerary/solution/zhong-xin-an-pai-xing-cheng-by-leetcode-solution/
     * @param tickets
     * @return
     */
    public List<String> findItinerary(List<List<String>> tickets) {
        for (List<String> ticket : tickets) {
            String src = ticket.get(0);
            String dst = ticket.get(1);
            if(!map.containsKey(src)){
                map.put(src,new PriorityQueue<String>());
            }
            map.get(src).offer(dst);
        }
        dfs("JFK");
        Collections.reverse(res);
        return res;
    }

    public void dfs(String str){
        while (map.containsKey(str) && map.get(str).size()>0){
            String poll = map.get(str).poll();
            dfs(poll);
        }
        res.add(str);
    }


    /**
     * 解题思路：
     * 首先，我们忽略按照字典顺序排列这一条件，那么这道题本质上求得的是有向图的欧拉路径。
     *
     *
     * 严谨地说，一个连通有向图 G 有欧拉路径，指存在一个顶点，从它出发，沿着有向边的方向，可以不重复地遍历图中所有的边。
     *
     * https://zh.wikipedia.org/wiki/%E4%B8%80%E7%AC%94%E7%94%BB%E9%97%AE%E9%A2%98
     * 题目中给定的机场名称是图的顶点，行程是图的边。题目要求重新安排行程，从示例可以看出每个行程都必须用到且只用一次。对应到欧拉路径的定义，每条边都要走到，且不重复。那么，这道题就转化成了给定起点，求一条字典顺序最小的欧拉路径。为了引出解法，我们先放几个例子。
     *
     *
     *
     * 图 1 展示了一张顶点度数都为偶数的图，首先我们忽略掉按字典顺序输出的条件。我们可以看出，如果顶点度数为偶数，那么我们先从 JFK 到 MUC 再回 JFK 到 ATL 最后返回 JFK，又或是 JFK 先到 ATL 再回 JFK 再去 MUC 再回 JFK，都是合法的路径。如果按照字典顺序输出，我们 优先访问字典顺序小的节点ATL即可。因此，我们 使用贪心策略，优先访问字典顺序小的顶点。
     *
     *
     *
     * 图 2 这个例子可以看出，我们别无选择必须先从 JFK 到 NRT 再回 JFK，最后到达 KUL 作为终点。如果我们按照字典顺序先到 KUL，就进入了 “死路”。但是上一个例子我们提到了，优先访问字典顺序小的顶点，那么我们第一次肯定是先到 KUL，这就走不通了，那怎么解决呢？当我们采用 DFS 方式遍历图时，需要将访问到的节点逆序插入到结果集。因此第一个访问到的节点将出现在结果集最后面，而我们是以顺序的方式来查看结果。如果第一个访问的节点是 “孤岛节点”，他会出现在结果集的最后。当我们顺序读取结果集时，这种 “孤岛节点” 是最后遇到的，是图遍历的终点，这样就没有问题了。
     *
     *
     *
     * 我们在图 3 绘制了算法执行过程，黑色实线表示图的边；红色实实线表示递归调用；绿色虚线表示递归调用返回；数字代表执行顺序；文字表示执行的操作，结果集的数字表示在第几步操作加入的。我们从 JFK 出发，沿着边到达 KUL（因为 KUL 字典顺序比 NRT 小），然后 KUL 没有临接点，将它放入结果集(2)，然后从 KUL 返回到达 JFK，注意这个是通过调用栈返回而不是沿着边返回。然后从 JFK 出发沿着边到达NRT，因为 NRT 到 JFK 有返回边，沿着边再回到 JFK。此时 JFK 的两个临接点都访问过了，我们将 JFK 加入结果集(6)。然后我们从 JFK 返回到 NRT，这是从调用栈返回。然后 NRT 的临接点都访问过了，我们将 NRT 加入结果集(8)，然后退栈回到 JFK。JFK 的所有临接点都访问过了，将 JFK 加入结果集(10)，然后退栈，整个流程结束。
     *
     * 在实现方面，我们使用 Map<String, List<String>> 存储图，Key 为顶点，List<String> 为临接点。为了避免存在环路导致节点重复访问，我们每访问过一条边就把它标记为访问过，或者直接将访问过的边删除。为了实现按照字典顺序访问，我们把每个顶点的临接点按照字典顺序排序。这里，我们直接将访问过的边删除，然后每次都取临接点的第一个即可满足字典顺序访问。
     *
     * 因为每个顶点都要访问一次，每条边都要访问一次，时间复杂度应为 O(|V|+|E|)O(∣V∣+∣E∣)，还要记得对临接点排序的时间复杂度 O(|E|log|E|)O(∣E∣log∣E∣)，算法整体时间复杂度为 O(|E|log|E|)O(∣E∣log∣E∣)；如果整个图是链式的，那么调用栈最深，空间复杂度应为 O(|E|)O(∣E∣)。
     *
     * 作者：pwrliang
     * 链接：https://leetcode-cn.com/problems/reconstruct-itinerary/solution/javadfsjie-fa-by-pwrliang/
     * @param tickets
     * @return
     */
    public List<String> findItinerary_2(List<List<String>> tickets) {
        // 因为逆序插入，所以用链表
        List<String> ans = new LinkedList<>();
        if (tickets == null || tickets.size() == 0) {
            return ans;
        }
        Map<String, PriorityQueue<String>> graph = new HashMap<>();
        for (List<String> pair : tickets) {
            // 因为涉及删除操作，我们用链表
            PriorityQueue<String> nbr = graph.computeIfAbsent(pair.get(0), k -> new PriorityQueue<>());
            nbr.add(pair.get(1));
        }
        visit(graph, "JFK", ans);
        return ans;
    }
    // DFS方式遍历图，当走到不能走为止，再将节点加入到答案
    private void visit(Map<String, PriorityQueue<String>> graph, String src, List<String> ans) {
        PriorityQueue<String> nbr = graph.get(src);
        while (nbr != null && nbr.size() > 0) {
            String dest = nbr.poll();
            visit(graph, dest, ans);
        }
        ans.add(0, src); // 逆序插入
    }

}
